diff --git a/swh/web/browse/urls.py b/swh/web/browse/urls.py index 3379ac71..47961434 100644 --- a/swh/web/browse/urls.py +++ b/swh/web/browse/urls.py @@ -1,42 +1,42 @@ # Copyright (C) 2017 The Software Heritage developers # See the AUTHORS file at the top-level directory of this distribution # License: GNU General Public License version 3, or any later version # See top-level LICENSE file for more information from django.conf.urls import url from django.shortcuts import redirect from swh.web.common.utils import reverse import swh.web.browse.views.directory # noqa import swh.web.browse.views.content # noqa import swh.web.browse.views.origin # noqa import swh.web.browse.views.person # noqa import swh.web.browse.views.revision # noqa from swh.web.browse.browseurls import BrowseUrls def default_browse_view(request): """Default django view used as an entry point for the swh browse ui web application. The url that point to it is /browse/. Currently, it points to the origin view for the linux kernel source tree github mirror. Args: request: input django http request """ linux_origin_id = '2' linux_origin_url = reverse('browse-origin', kwargs={'origin_id': linux_origin_id}) return redirect(linux_origin_url) urlpatterns = [ - url(r'^$', default_browse_view) + url(r'^$', default_browse_view, name='browse-homepage') ] urlpatterns += BrowseUrls.get_url_patterns() diff --git a/swh/web/browse/views/content.py b/swh/web/browse/views/content.py index bcd43eea..88cd7847 100644 --- a/swh/web/browse/views/content.py +++ b/swh/web/browse/views/content.py @@ -1,155 +1,155 @@ # Copyright (C) 2017 The Software Heritage developers # See the AUTHORS file at the top-level directory of this distribution # License: GNU General Public License version 3, or any later version # See top-level LICENSE file for more information from django.http import HttpResponse from django.shortcuts import render from django.template.defaultfilters import filesizeformat from swh.model.hashutil import hash_to_hex from swh.web.common import query from swh.web.common.utils import reverse from swh.web.common.exc import handle_view_exception from swh.web.browse.utils import ( gen_path_info, request_content, prepare_content_for_display ) from swh.web.browse.browseurls import browse_route @browse_route(r'content/(?P.+)/raw/', view_name='browse-content-raw') def content_raw(request, query_string): """Django view that produces a raw display of a SWH content identified by its hash value. The url that points to it is :http:get:`/browse/content/[(algo_hash):](hash)/raw/` Args: request: input django http request query_string: a string of the form "[ALGO_HASH:]HASH" where optional ALGO_HASH can be either *sha1*, *sha1_git*, *sha256*, or *blake2s256* (default to *sha1*) and HASH the hexadecimal representation of the hash value Returns: The raw bytes of the content. """ # noqa try: algo, checksum = query.parse_hash(query_string) checksum = hash_to_hex(checksum) content_data = request_content(query_string) except Exception as exc: - return handle_view_exception(exc) + return handle_view_exception(request, exc) filename = request.GET.get('filename', None) if not filename: filename = '%s_%s' % (algo, checksum) if content_data['mimetype'].startswith('text/'): response = HttpResponse(content_data['raw_data'], content_type="text/plain") response['Content-disposition'] = 'filename=%s' % filename else: response = HttpResponse(content_data['raw_data'], content_type='application/octet-stream') response['Content-disposition'] = 'attachment; filename=%s' % filename return response @browse_route(r'content/(?P.+)/', view_name='browse-content') def content_display(request, query_string): """Django view that produces an HTML display of a SWH content identified by its hash value. The url that points to it is :http:get:`/browse/content/[(algo_hash):](hash)/` Args: request: input django http request query_string: a string of the form "[ALGO_HASH:]HASH" where optional ALGO_HASH can be either *sha1*, *sha1_git*, *sha256*, or *blake2s256* (default to *sha1*) and HASH the hexadecimal representation of the hash value Returns: The HTML rendering of the requested content. """ # noqa try: algo, checksum = query.parse_hash(query_string) checksum = hash_to_hex(checksum) content_data = request_content(query_string) except Exception as exc: - return handle_view_exception(exc) + return handle_view_exception(request, exc) path = request.GET.get('path', None) content_display_data = prepare_content_for_display( content_data['raw_data'], content_data['mimetype'], path) root_dir = None filename = None path_info = None breadcrumbs = [] if path: split_path = path.split('/') root_dir = split_path[0] filename = split_path[-1] path = path.replace(root_dir + '/', '') path = path.replace(filename, '') path_info = gen_path_info(path) breadcrumbs.append({'name': root_dir[:7], 'url': reverse('browse-directory', kwargs={'sha1_git': root_dir})}) for pi in path_info: breadcrumbs.append({'name': pi['name'], 'url': reverse('browse-directory', kwargs={'sha1_git': root_dir, 'path': pi['path']})}) breadcrumbs.append({'name': filename, 'url': None}) query_params = None if filename: query_params = {'filename': filename} content_raw_url = reverse('browse-content-raw', kwargs={'query_string': query_string}, query_params=query_params) content_metadata = { 'sha1 checksum': content_data['checksums']['sha1'], 'sha1_git checksum': content_data['checksums']['sha1_git'], 'sha256 checksum': content_data['checksums']['sha256'], 'blake2s256 checksum': content_data['checksums']['blake2s256'], 'mime type': content_data['mimetype'], 'encoding': content_data['encoding'], 'size': filesizeformat(content_data['length']), 'language': content_data['language'], 'licenses': content_data['licenses'] } return render(request, 'content.html', {'top_panel_visible': True, 'top_panel_collapsible': True, 'top_panel_text_left': 'SWH object: Content', 'top_panel_text_right': '%s: %s' % (algo, checksum), 'swh_object_metadata': content_metadata, 'main_panel_visible': True, 'content': content_display_data['content_data'], 'mimetype': content_data['mimetype'], 'language': content_display_data['language'], 'breadcrumbs': breadcrumbs, 'branches': None, 'branch': None, 'top_right_link': content_raw_url, 'top_right_link_text': 'Raw File'}) diff --git a/swh/web/browse/views/directory.py b/swh/web/browse/views/directory.py index 93be6bf9..8e874651 100644 --- a/swh/web/browse/views/directory.py +++ b/swh/web/browse/views/directory.py @@ -1,97 +1,97 @@ # Copyright (C) 2017 The Software Heritage developers # See the AUTHORS file at the top-level directory of this distribution # License: GNU General Public License version 3, or any later version # See top-level LICENSE file for more information from django.shortcuts import render from django.template.defaultfilters import filesizeformat from swh.web.common import service from swh.web.common.utils import reverse from swh.web.common.exc import handle_view_exception from swh.web.browse.utils import ( gen_path_info, get_directory_entries ) from swh.web.browse.browseurls import browse_route @browse_route(r'directory/(?P[0-9a-f]+)/', r'directory/(?P[0-9a-f]+)/(?P.+)/', view_name='browse-directory') def directory_browse(request, sha1_git, path=None): """Django view for browsing the content of a SWH directory identified by its sha1_git value. The url that points to it is :http:get:`/browse/directory/(sha1_git)/[(path)/]` Args: request: input django http request sha1_git: swh sha1_git identifer of the directory to browse path: optionnal path parameter used to navigate in directories reachable from the provided root one Returns: The HTML rendering for the content of the provided directory. """ # noqa root_sha1_git = sha1_git try: if path: dir_info = service.lookup_directory_with_path(sha1_git, path) sha1_git = dir_info['target'] dirs, files = get_directory_entries(sha1_git) except Exception as exc: - return handle_view_exception(exc) + return handle_view_exception(request, exc) path_info = gen_path_info(path) breadcrumbs = [] breadcrumbs.append({'name': root_sha1_git[:7], 'url': reverse('browse-directory', kwargs={'sha1_git': root_sha1_git})}) for pi in path_info: breadcrumbs.append({'name': pi['name'], 'url': reverse('browse-directory', kwargs={'sha1_git': root_sha1_git, 'path': pi['path']})}) path = '' if path is None else (path + '/') for d in dirs: d['url'] = reverse('browse-directory', kwargs={'sha1_git': root_sha1_git, 'path': path + d['name']}) sum_file_sizes = 0 for f in files: query_string = 'sha1_git:' + f['target'] f['url'] = reverse('browse-content', kwargs={'query_string': query_string}, query_params={'path': root_sha1_git + '/' + path + f['name']}) sum_file_sizes += f['length'] f['length'] = filesizeformat(f['length']) sum_file_sizes = filesizeformat(sum_file_sizes) dir_metadata = {'id': sha1_git, 'number of regular files': len(files), 'number of subdirectories': len(dirs), 'sum of regular file sizes': sum_file_sizes} return render(request, 'directory.html', {'top_panel_visible': True, 'top_panel_collapsible': True, 'top_panel_text_left': 'SWH object: Directory', 'top_panel_text_right': 'Sha1 git: ' + sha1_git, 'swh_object_metadata': dir_metadata, 'main_panel_visible': True, 'dirs': dirs, 'files': files, 'breadcrumbs': breadcrumbs, 'branches': None, 'branch': None, 'top_right_link': None, 'top_right_link_text': None}) diff --git a/swh/web/browse/views/origin.py b/swh/web/browse/views/origin.py index 21623545..484fc983 100644 --- a/swh/web/browse/views/origin.py +++ b/swh/web/browse/views/origin.py @@ -1,583 +1,583 @@ # Copyright (C) 2017 The Software Heritage developers # See the AUTHORS file at the top-level directory of this distribution # License: GNU General Public License version 3, or any later version # See top-level LICENSE file for more information import dateutil from django.shortcuts import render from django.template.defaultfilters import filesizeformat from swh.web.common import service from swh.web.common.utils import reverse, format_utc_iso_date from swh.web.common.exc import NotFoundExc, handle_view_exception from swh.web.browse.utils import ( get_origin_visits, get_origin_visit_branches, gen_path_info, get_directory_entries, request_content, prepare_content_for_display, gen_link, prepare_revision_log_for_display ) from swh.web.browse.browseurls import browse_route @browse_route(r'origin/(?P[0-9]+)/', r'origin/(?P[a-z]+)/url/(?P.+)/', view_name='browse-origin') def origin_browse(request, origin_id=None, origin_type=None, origin_url=None): """Django view that produces an HTML display of a swh origin identified by its id or its url. The url scheme that points to it is :http:get:`/browse/origin/(origin_id)/`. Args: request: input django http request origin_id: a swh origin id origin_type: type of origin (git, svn, ...) origin_url: url of the origin (e.g. https://github.com//) Returns: The HMTL rendering for the metadata of the provided origin. """ # noqa try: if origin_id: origin_request_params = { 'id': origin_id, } else: origin_request_params = { 'type': origin_type, 'url': origin_url } origin_info = service.lookup_origin(origin_request_params) origin_id = origin_info['id'] origin_visits = get_origin_visits(origin_id) except Exception as exc: - return handle_view_exception(exc) + return handle_view_exception(request, exc) origin_info['last swh visit browse url'] = \ reverse('browse-origin-directory', kwargs={'origin_id': origin_id}) origin_visits_data = [] for visit in origin_visits: visit_date = dateutil.parser.parse(visit['date']) visit['date'] = format_utc_iso_date(visit['date']) visit['browse_url'] = reverse('browse-origin-directory', kwargs={'origin_id': origin_id, 'visit_id': visit['visit']}) origin_visits_data.append( {'date': visit_date.timestamp()}) return render(request, 'origin.html', {'top_panel_visible': True, 'top_panel_collapsible': True, 'top_panel_text_left': 'SWH object: Origin', 'top_panel_text_right': 'Url: ' + origin_info['url'], 'swh_object_metadata': origin_info, 'main_panel_visible': True, 'origin_visits_data': origin_visits_data, 'visits': list(reversed(origin_visits)), 'browse_url_base': '/browse/origin/%s/' % origin_id}) def _get_origin_branches_and_url_args(origin_id, visit_id, ts): if not visit_id and ts: branches = get_origin_visit_branches(origin_id, visit_ts=ts) url_args = {'origin_id': origin_id, 'timestamp': ts} else: branches = get_origin_visit_branches(origin_id, visit_id) url_args = {'origin_id': origin_id, 'visit_id': visit_id} return branches, url_args def _raise_exception_if_branch_not_found(origin_id, visit_id, ts, branch): if visit_id: raise NotFoundExc('Branch %s associated to visit with' ' id %s for origin with id %s' ' not found!' % (branch, visit_id, origin_id)) else: raise NotFoundExc('Branch %s associated to visit with' ' timestamp %s for origin with id %s' ' not found!' % (branch, ts, origin_id)) def _get_branch(branches, branch_name): """ Utility function to get a specific branch from an origin branches list. Its purpose is to get the default HEAD branch as some SWH origin (e.g those with svn type) does not have it. In that latter case, check if there is a master branch instead and returns it. """ filtered_branches = [b for b in branches if b['name'].endswith(branch_name)] # noqa if len(filtered_branches) > 0: return filtered_branches[0] elif branch_name == 'HEAD': filtered_branches = [b for b in branches if b['name'].endswith('master')] # noqa if len(filtered_branches) > 0: return filtered_branches[0] return None @browse_route(r'origin/(?P[0-9]+)/directory/', r'origin/(?P[0-9]+)/directory/(?P.+)/', r'origin/(?P[0-9]+)/visit/(?P[0-9]+)/directory/', # noqa r'origin/(?P[0-9]+)/visit/(?P[0-9]+)/directory/(?P.+)/', # noqa r'origin/(?P[0-9]+)/ts/(?P.+)/directory/', # noqa r'origin/(?P[0-9]+)/ts/(?P.+)/directory/(?P.+)/', # noqa view_name='browse-origin-directory') def origin_directory_browse(request, origin_id, visit_id=None, timestamp=None, path=None): """Django view for browsing the content of a swh directory associated to an origin for a given visit. The url scheme that points to it is the following: * :http:get:`/browse/origin/(origin_id)/directory/[(path)/]` * :http:get:`/browse/origin/(origin_id)/visit/(visit_id)/directory/[(path)/]` * :http:get:`/browse/origin/(origin_id)/ts/(timestamp)/directory/[(path)/]` Args: request: input django http request origin_id: a swh origin id visit_id: optionnal visit id parameter (the last one will be used by default) timestamp: optionnal visit timestamp parameter (the last one will be used by default) path: optionnal path parameter used to navigate in directories reachable from the origin root one branch: optionnal query parameter that specifies the origin branch from which to retrieve the directory revision: optional query parameter to specify the origin revision from which to retrieve the directory Returns: The HTML rendering for the content of the directory associated to the provided origin and visit. """ # noqa try: if not visit_id and not timestamp: origin_visits = get_origin_visits(origin_id) if not origin_visits: raise NotFoundExc('No SWH visit associated to ' 'origin with id %s' % origin_id) return origin_directory_browse(request, origin_id, origin_visits[-1]['visit'], path=path) origin_info = service.lookup_origin({'id': origin_id}) branches, url_args = _get_origin_branches_and_url_args(origin_id, visit_id, timestamp) for b in branches: branch_url_args = dict(url_args) if path: b['path'] = path branch_url_args['path'] = path b['url'] = reverse('browse-origin-directory', kwargs=branch_url_args, query_params={'branch': b['name']}) revision_id = request.GET.get('revision', None) if revision_id: revision = service.lookup_revision(revision_id) root_sha1_git = revision['directory'] branches.append({'name': revision_id, 'revision': revision_id, 'directory': root_sha1_git, 'url': None}) branch_name = revision_id else: branch_name = request.GET.get('branch', 'HEAD') branch = _get_branch(branches, branch_name) if branch: branch_name = branch['name'] root_sha1_git = branch['directory'] else: _raise_exception_if_branch_not_found(origin_id, visit_id, timestamp, branch_name) sha1_git = root_sha1_git if path: dir_info = service.lookup_directory_with_path(root_sha1_git, path) sha1_git = dir_info['target'] dirs, files = get_directory_entries(sha1_git) except Exception as exc: - return handle_view_exception(exc) + return handle_view_exception(request, exc) if revision_id: query_params = {'revision': revision_id} else: query_params = {'branch': branch_name} path_info = gen_path_info(path) breadcrumbs = [] breadcrumbs.append({'name': root_sha1_git[:7], 'url': reverse('browse-origin-directory', kwargs=url_args, query_params=query_params)}) for pi in path_info: bc_url_args = dict(url_args) bc_url_args['path'] = pi['path'] breadcrumbs.append({'name': pi['name'], 'url': reverse('browse-origin-directory', kwargs=bc_url_args, query_params=query_params)}) path = '' if path is None else (path + '/') for d in dirs: bc_url_args = dict(url_args) bc_url_args['path'] = path + d['name'] d['url'] = reverse('browse-origin-directory', kwargs=bc_url_args, query_params=query_params) sum_file_sizes = 0 for f in files: bc_url_args = dict(url_args) bc_url_args['path'] = path + f['name'] f['url'] = reverse('browse-origin-content', kwargs=bc_url_args, query_params=query_params) sum_file_sizes += f['length'] f['length'] = filesizeformat(f['length']) history_url = reverse('browse-origin-log', kwargs=url_args, query_params=query_params) sum_file_sizes = filesizeformat(sum_file_sizes) dir_metadata = {'id': sha1_git, 'number of regular files': len(files), 'number of subdirectories': len(dirs), 'sum of regular file sizes': sum_file_sizes, 'origin id': origin_info['id'], 'origin type': origin_info['type'], 'origin url': origin_info['url'], 'path': '/' + path} return render(request, 'directory.html', {'top_panel_visible': True, 'top_panel_collapsible': True, 'top_panel_text_left': 'SWH object: Directory', 'top_panel_text_right': 'Origin: ' + origin_info['url'], 'swh_object_metadata': dir_metadata, 'main_panel_visible': True, 'dirs': dirs, 'files': files, 'breadcrumbs': breadcrumbs, 'branches': branches, 'branch': branch_name, 'top_right_link': history_url, 'top_right_link_text': 'History'}) @browse_route(r'origin/(?P[0-9]+)/content/(?P.+)/', r'origin/(?P[0-9]+)/visit/(?P[0-9]+)/content/(?P.+)/', # noqa r'origin/(?P[0-9]+)/ts/(?P.+)/content/(?P.+)/', # noqa view_name='browse-origin-content') def origin_content_display(request, origin_id, path, visit_id=None, timestamp=None): """Django view that produces an HTML display of a swh content associated to an origin for a given visit. The url scheme that points to it is the following: * :http:get:`/browse/origin/(origin_id)/content/(path)/` * :http:get:`/browse/origin/(origin_id)/visit/(visit_id)/content/(path)/` * :http:get:`/browse/origin/(origin_id)/ts/(timestamp)/content/(path)/` Args: request: input django http request origin_id: id of a swh origin path: path of the content relative to the origin root directory visit_id: optionnal visit id parameter (the last one will be used by default) timestamp: optionnal visit timestamp parameter (the last one will be used by default) branch: optionnal query parameter that specifies the origin branch from which to retrieve the content revision: optional query parameter to specify the origin revision from which to retrieve the content Returns: The HTML rendering of the requested content associated to the provided origin and visit. """ # noqa try: if not visit_id and not timestamp: origin_visits = get_origin_visits(origin_id) if not origin_visits: raise NotFoundExc('No SWH visit associated to ' 'origin with id %s' % origin_id) return origin_content_display(request, origin_id, path, origin_visits[-1]['visit']) origin_info = service.lookup_origin({'id': origin_id}) branches, url_args = _get_origin_branches_and_url_args(origin_id, visit_id, timestamp) for b in branches: bc_url_args = dict(url_args) bc_url_args['path'] = path b['url'] = reverse('browse-origin-content', kwargs=bc_url_args, query_params={'branch': b['name']}) revision_id = request.GET.get('revision', None) if revision_id: revision = service.lookup_revision(revision_id) root_sha1_git = revision['directory'] branches.append({'name': revision_id, 'revision': revision_id, 'directory': root_sha1_git, 'url': None}) branch_name = revision_id else: branch_name = request.GET.get('branch', 'HEAD') branch = _get_branch(branches, branch_name) if branch: branch_name = branch['name'] root_sha1_git = branch['directory'] else: _raise_exception_if_branch_not_found(origin_id, visit_id, timestamp, branch_name) content_info = service.lookup_directory_with_path(root_sha1_git, path) sha1_git = content_info['target'] query_string = 'sha1_git:' + sha1_git content_data = request_content(query_string) except Exception as exc: - return handle_view_exception(exc) + return handle_view_exception(request, exc) if revision_id: query_params = {'revision': revision_id} else: query_params = {'branch': branch_name} content_display_data = prepare_content_for_display( content_data['raw_data'], content_data['mimetype'], path) filename = None path_info = None breadcrumbs = [] split_path = path.split('/') filename = split_path[-1] path = path.replace(filename, '') path_info = gen_path_info(path) breadcrumbs.append({'name': root_sha1_git[:7], 'url': reverse('browse-origin-directory', kwargs=url_args, query_params=query_params)}) for pi in path_info: bc_url_args = dict(url_args) bc_url_args['path'] = pi['path'] breadcrumbs.append({'name': pi['name'], 'url': reverse('browse-origin-directory', kwargs=bc_url_args, query_params=query_params)}) breadcrumbs.append({'name': filename, 'url': None}) content_raw_url = reverse('browse-content-raw', kwargs={'query_string': query_string}, query_params={'filename': filename}) content_metadata = { 'sha1 checksum': content_data['checksums']['sha1'], 'sha1_git checksum': content_data['checksums']['sha1_git'], 'sha256 checksum': content_data['checksums']['sha256'], 'blake2s256 checksum': content_data['checksums']['blake2s256'], 'mime type': content_data['mimetype'], 'encoding': content_data['encoding'], 'size': filesizeformat(content_data['length']), 'language': content_data['language'], 'licenses': content_data['licenses'], 'origin id': origin_info['id'], 'origin type': origin_info['type'], 'origin url': origin_info['url'], 'path': '/' + path, 'filename': filename } return render(request, 'content.html', {'top_panel_visible': True, 'top_panel_collapsible': True, 'top_panel_text_left': 'SWH object: Content', 'top_panel_text_right': 'Origin: %s' % origin_info['url'], 'swh_object_metadata': content_metadata, 'main_panel_visible': True, 'content': content_display_data['content_data'], 'mimetype': content_data['mimetype'], 'language': content_display_data['language'], 'breadcrumbs': breadcrumbs, 'branches': branches, 'branch': branch_name, 'top_right_link': content_raw_url, 'top_right_link_text': 'Raw File'}) def _gen_directory_link(url_args, revision, link_text): directory_url = reverse('browse-origin-directory', kwargs=url_args, query_params={'revision': revision}) return gen_link(directory_url, link_text) NB_LOG_ENTRIES = 20 @browse_route(r'origin/(?P[0-9]+)/log/', r'origin/(?P[0-9]+)/visit/(?P[0-9]+)/log/', # noqa r'origin/(?P[0-9]+)/ts/(?P.+)/log/', view_name='browse-origin-log') def origin_log_browse(request, origin_id, visit_id=None, timestamp=None): - """Django view that produces an HTML display of revisions history (aka + """Django view that produces an HTML display of revisions history (aka the commit log) associated to a SWH origin. The url scheme that points to it is the following: * :http:get:`/browse/origin/(origin_id)/log/` * :http:get:`/browse/origin/(origin_id)/visit/(visit_id)/log/` * :http:get:`/browse/origin/(origin_id)/ts/(timestamp)/log/` Args: request: input django http request origin_id: id of a swh origin visit_id: optionnal visit id parameter (the last one will be used by default) timestamp: optionnal visit timestamp parameter (the last one will be used by default) - revs_breadcrumb: query parameter used internally to store + revs_breadcrumb: query parameter used internally to store the navigation breadcrumbs (i.e. the list of descendant revisions visited so far). per_page: optionnal query parameter used to specify the number of log entries per page branch: optionnal query parameter that specifies the origin branch from which to retrieve the content revision: optional query parameter to specify the origin revision from which to retrieve the directory Returns: The HTML rendering of revisions history for a given SWH visit. """ # noqa try: if not visit_id and not timestamp: origin_visits = get_origin_visits(origin_id) if not origin_visits: raise NotFoundExc('No SWH visit associated to ' 'origin with id %s' % origin_id) return origin_log_browse(request, origin_id, origin_visits[-1]['visit']) branches, url_args = _get_origin_branches_and_url_args(origin_id, visit_id, timestamp) for b in branches: b['url'] = reverse('browse-origin-log', kwargs=url_args, query_params={'branch': b['name']}) revision_id = request.GET.get('revision', None) revs_breadcrumb = request.GET.get('revs_breadcrumb', None) branch_name = request.GET.get('branch', 'HEAD') if revision_id: revision = service.lookup_revision(revision_id) branches.append({'name': revision_id, 'revision': revision_id, 'directory': revision['directory']}) revision = revision_id branch_name = revision_id elif revs_breadcrumb: revs = revs_breadcrumb.split('/') revision = revs[-1] else: branch = _get_branch(branches, branch_name) if branch: branch_name = branch['name'] revision = branch['revision'] else: _raise_exception_if_branch_not_found(origin_id, visit_id, timestamp, branch_name) per_page = int(request.GET.get('per_page', NB_LOG_ENTRIES)) revision_log = service.lookup_revision_log(revision, limit=per_page+1) revision_log = list(revision_log) except Exception as exc: - return handle_view_exception(exc) + return handle_view_exception(request, exc) revision_log_display_data = prepare_revision_log_for_display( revision_log, per_page, revs_breadcrumb, origin_context=True) prev_rev = revision_log_display_data['prev_rev'] prev_revs_breadcrumb = revision_log_display_data['prev_revs_breadcrumb'] prev_log_url = None if prev_rev: prev_log_url = \ reverse('browse-origin-log', kwargs=url_args, query_params={'revs_breadcrumb': prev_revs_breadcrumb, 'per_page': per_page, 'branch': branch_name}) next_rev = revision_log_display_data['next_rev'] next_revs_breadcrumb = revision_log_display_data['next_revs_breadcrumb'] next_log_url = None if next_rev: next_log_url = \ reverse('browse-origin-log', kwargs=url_args, query_params={'revs_breadcrumb': next_revs_breadcrumb, 'per_page': per_page, 'branch': branch_name}) revision_log_data = revision_log_display_data['revision_log_data'] for i, log in enumerate(revision_log_data): log['directory'] = _gen_directory_link(url_args, revision_log[i]['id'], 'Tree') return render(request, 'revision-log.html', {'top_panel_visible': False, 'top_panel_collapsible': False, 'top_panel_text_left': 'SWH object: Revision history', 'top_panel_text_right': 'Sha1 git: ' + revision, 'swh_object_metadata': None, 'main_panel_visible': True, 'revision_log': revision_log_data, 'next_log_url': next_log_url, 'prev_log_url': prev_log_url, 'breadcrumbs': None, 'branches': branches, 'branch': branch_name, 'top_right_link': None, 'top_right_link_text': None, 'include_top_navigation': True}) diff --git a/swh/web/browse/views/person.py b/swh/web/browse/views/person.py index 1b77cb03..d79b1e53 100644 --- a/swh/web/browse/views/person.py +++ b/swh/web/browse/views/person.py @@ -1,40 +1,40 @@ # Copyright (C) 2017 The Software Heritage developers # See the AUTHORS file at the top-level directory of this distribution # License: GNU General Public License version 3, or any later version # See top-level LICENSE file for more information from django.shortcuts import render from swh.web.common import service from swh.web.common.exc import handle_view_exception from swh.web.browse.browseurls import browse_route @browse_route(r'person/(?P[0-9]+)/', view_name='browse-person') def person_browse(request, person_id): """ Django view that produces an HTML display of a swh person identified by its id. The url that points to it is :http:get:`/browse/person/(person_id)/`. Args: request: input django http request person_id (int): a swh person id Returns: The HMTL rendering for the metadata of the provided person. """ try: person = service.lookup_person(person_id) except Exception as exc: - return handle_view_exception(exc) + return handle_view_exception(request, exc) return render(request, 'person.html', {'top_panel_visible': True, 'top_panel_collapsible': False, 'top_panel_text_left': 'SWH object: Person', 'top_panel_text_right': 'Name: ' + person['name'], 'swh_object_metadata': person, 'main_panel_visible': False}) diff --git a/swh/web/browse/views/revision.py b/swh/web/browse/views/revision.py index 7a492e0c..c49b8fd4 100644 --- a/swh/web/browse/views/revision.py +++ b/swh/web/browse/views/revision.py @@ -1,163 +1,163 @@ # Copyright (C) 2017 The Software Heritage developers # See the AUTHORS file at the top-level directory of this distribution # License: GNU General Public License version 3, or any later version # See top-level LICENSE file for more information import json from django.shortcuts import render from django.utils.safestring import mark_safe from swh.web.common import service from swh.web.common.utils import reverse, format_utc_iso_date from swh.web.common.exc import handle_view_exception from swh.web.browse.browseurls import browse_route from swh.web.browse.utils import ( gen_link, gen_person_link, gen_revision_link, prepare_revision_log_for_display ) def _gen_directory_link(sha1_git, link_text): directory_url = reverse('browse-directory', kwargs={'sha1_git': sha1_git}) return gen_link(directory_url, link_text) def _gen_revision_log_link(revision_id): revision_log_url = reverse('browse-revision-log', kwargs={'sha1_git': revision_id}) return gen_link(revision_log_url, revision_log_url) @browse_route(r'revision/(?P[0-9a-f]+)/', view_name='browse-revision') def revision_browse(request, sha1_git): """ Django view that produces an HTML display of a SWH revision identified by its id. The url that points to it is :http:get:`/browse/revision/(sha1_git)/`. Args: request: input django http request sha1_git: a SWH revision id Returns: The HMTL rendering for the metadata of the provided revision. """ try: revision = service.lookup_revision(sha1_git) except Exception as exc: - return handle_view_exception(exc) + return handle_view_exception(request, exc) revision_data = {} revision_data['author'] = gen_person_link( revision['author']['id'], revision['author']['name']) revision_data['committer'] = gen_person_link( revision['committer']['id'], revision['committer']['name']) revision_data['committer date'] = format_utc_iso_date( revision['committer_date']) revision_data['date'] = format_utc_iso_date(revision['date']) revision_data['directory'] = _gen_directory_link(revision['directory'], revision['directory']) revision_data['history log'] = _gen_revision_log_link(sha1_git) revision_data['id'] = sha1_git revision_data['merge'] = revision['merge'] revision_data['message'] = revision['message'] revision_data['metadata'] = json.dumps(revision['metadata'], sort_keys=True, indent=4, separators=(',', ': ')) parents = '' for p in revision['parents']: parent_link = gen_revision_link(p) parents += parent_link + '
' revision_data['parents'] = mark_safe(parents) revision_data['synthetic'] = revision['synthetic'] revision_data['type'] = revision['type'] return render(request, 'revision.html', {'top_panel_visible': False, 'top_panel_collapsible': False, 'top_panel_text_left': 'SWH object: Revision', 'top_panel_text_right': 'Sha1 git: ' + sha1_git, 'swh_object_metadata': None, 'main_panel_visible': True, 'revision': revision_data}) NB_LOG_ENTRIES = 20 @browse_route(r'revision/(?P[0-9a-f]+)/log/', view_name='browse-revision-log') def revision_log_browse(request, sha1_git): """ Django view that produces an HTML display of the history log for a SWH revision identified by its id. The url that points to it is :http:get:`/browse/revision/(sha1_git)/log/`. Args: request: input django http request sha1_git: a SWH revision id Returns: The HMTL rendering of the revision history log. """ # noqa try: per_page = int(request.GET.get('per_page', NB_LOG_ENTRIES)) revision_log = service.lookup_revision_log(sha1_git, limit=per_page+1) revision_log = list(revision_log) except Exception as exc: - return handle_view_exception(exc) + return handle_view_exception(request, exc) revs_breadcrumb = request.GET.get('revs_breadcrumb', None) revision_log_display_data = prepare_revision_log_for_display( revision_log, per_page, revs_breadcrumb) prev_rev = revision_log_display_data['prev_rev'] prev_revs_breadcrumb = revision_log_display_data['prev_revs_breadcrumb'] prev_log_url = None if prev_rev: prev_log_url = \ reverse('browse-revision-log', kwargs={'sha1_git': prev_rev}, query_params={'revs_breadcrumb': prev_revs_breadcrumb, 'per_page': per_page}) next_rev = revision_log_display_data['next_rev'] next_revs_breadcrumb = revision_log_display_data['next_revs_breadcrumb'] next_log_url = None if next_rev: next_log_url = \ reverse('browse-revision-log', kwargs={'sha1_git': next_rev}, query_params={'revs_breadcrumb': next_revs_breadcrumb, 'per_page': per_page}) revision_log_data = revision_log_display_data['revision_log_data'] for log in revision_log_data: log['directory'] = _gen_directory_link(log['directory'], 'Tree') return render(request, 'revision-log.html', {'top_panel_visible': False, 'top_panel_collapsible': False, 'top_panel_text_left': 'SWH object: Revision history', 'top_panel_text_right': 'Sha1 git: ' + sha1_git, 'swh_object_metadata': None, 'main_panel_visible': True, 'revision_log': revision_log_data, 'next_log_url': next_log_url, 'prev_log_url': prev_log_url, 'breadcrumbs': None, 'branches': None, 'branch': None, 'top_right_link': None, 'top_right_link_text': None, 'include_top_navigation': False}) diff --git a/swh/web/common/exc.py b/swh/web/common/exc.py index 21fe9203..abacc26d 100644 --- a/swh/web/common/exc.py +++ b/swh/web/common/exc.py @@ -1,54 +1,111 @@ # Copyright (C) 2015-2017 The Software Heritage developers # See the AUTHORS file at the top-level directory of this distribution # License: GNU Affero General Public License version 3, or any later version # See top-level LICENSE file for more information import traceback -from django.http import ( - HttpResponseBadRequest, HttpResponseNotFound -) +from django.shortcuts import render from swh.web.config import get_config class BadInputExc(ValueError): """Wrong request to the api. Example: Asking a content with the wrong identifier format. """ pass class NotFoundExc(Exception): """Good request to the api but no result were found. Example: Asking a content with the right identifier format but that content does not exist. """ pass class ForbiddenExc(Exception): """Good request to the api, forbidden result to return due to enforce policy. Example: Asking for a raw content which exists but whose mimetype is not text. """ pass -def handle_view_exception(exc): - content_type = None - content = str(exc) +_html_status_code_message = { + 400: "Bad Request", + 401: "Unauthorized", + 403: "Access Denied", + 404: "Resource not found", + 500: "Internal Server Error", + 501: "Not Implemented", + 502: "Bad Gateway", + 503: "Service unavailable" +} + + +def _generate_error_page(request, error_code, error_description): + return render(request, "error.html", + {'error_code': error_code, + 'error_message': _html_status_code_message[error_code], + 'error_description': error_description}, status=error_code) + + +def swh_handle400(request): + """ + Custom Django HTTP error 400 handler for swh-web. + """ + error_description = 'The server cannot process the request to %s due to '\ + 'something that is perceived to be a client error.' %\ + request.META['PATH_INFO'] + return _generate_error_page(request, 400, error_description) + + +def swh_handle403(request): + """ + Custom Django HTTP error 403 handler for swh-web. + """ + error_description = 'The resource %s requires an authentication.' %\ + request.META['PATH_INFO'] + return _generate_error_page(request, 403, error_description) + + +def swh_handle404(request): + """ + Custom Django HTTP error 404 handler for swh-web. + """ + error_description = 'The resource %s could not be found on the server.' %\ + request.META['PATH_INFO'] + return _generate_error_page(request, 404, error_description) + + +def swh_handle500(request): + """ + Custom Django HTTP error 500 handler for swh-web. + """ + error_description = 'An unexpected condition was encountered when '\ + 'requesting resource %s.' %\ + request.META['PATH_INFO'] + return _generate_error_page(request, 500, error_description) + + +def handle_view_exception(request, exc): + """ + Function used to generate an error page when an exception + was raised inside a swh-web browse view. + """ + error_code = 400 + error_description = str(exc) if get_config()['debug']: - content_type = 'text/plain' - content = traceback.format_exc() + error_description = traceback.format_exc() if isinstance(exc, NotFoundExc): - return HttpResponseNotFound(content, content_type=content_type) - else: - return HttpResponseBadRequest(content, content_type=content_type) + error_code = 404 + return _generate_error_page(request, error_code, error_description) diff --git a/swh/web/static/css/style.css b/swh/web/static/css/style.css index 53d3c273..f5d98616 100644 --- a/swh/web/static/css/style.css +++ b/swh/web/static/css/style.css @@ -1,421 +1,449 @@ /* version: 0.1 date: 21/09/15 author: swh email: swh website: softwareheritage.org version history: /style.css */ @import url(https://fonts.googleapis.com/css?family=Alegreya:400,400italic,700,700italic); @import url(https://fonts.googleapis.com/css?family=Alegreya+Sans:400,400italic,500,500italic,700,700italic,100,300,100italic,300italic); html { height: 100%; } body { font-family: 'Alegreya Sans', sans-serif; font-size: 1.7rem; line-height: 1.5; color: rgba(0, 0, 0, 0.55); padding-top: 80px; /* avoid fixed bootstrap navbar covers content */ padding-bottom: 120px; min-height: 100%; margin: 0; position: relative; } .heading { font-family: 'Alegreya', serif; } .shell, .text { font-size: 0.7em; } .logo img { max-height: 40px; } .logo .navbar-brand { padding: 5px; } .logo .sitename { padding: 15px 5px; } .jumbotron { padding: 0; background-color: rgba(0, 0, 0, 0); position: fixed; top: 0; width: 100%; } #swh-navbar-collapse { border-top-style: none; border-left-style: none; border-right-style: none; border-bottom: 5px solid; border-image: linear-gradient(to right, rgb(226, 0, 38) 0%, rgb(254, 205, 27) 100%) 1 1 1 1; width: 100%; padding: 5px; } .nav-horizontal { float: right; } h3[id], h4[id], a[id] { /* avoid in-page links covered by navbar */ padding-top: 80px; margin-top: -70px; } h1, h2, h3, h4 { margin: 0; color: #e20026; padding-bottom: 10px; } h1 { font-size: 1.8em; } h2 { font-size: 1.2em; } h3 { font-size: 1.1em; } a { color: rgba(0, 0, 0, 0.75); border-bottom-style: dotted; border-bottom-width: 1px; border-bottom-color: rgb(91, 94, 111); } a:hover { color: black; } ul.dropdown-menu a, .navbar-header a, ul.navbar-nav a { /* No decoration on links in dropdown menu */ border-bottom-style: none; color: #323232; font-weight: 700; } .navbar-header a:hover, ul.navbar-nav a:hover { color: #8f8f8f; } .sitename .first-word, .sitename .second-word { color: rgba(0, 0, 0, 0.75); font-weight: normal; font-size: 1.8rem; } .sitename .first-word { font-family: 'Alegreya Sans', sans-serif; } .sitename .second-word { font-family: 'Alegreya', serif; } ul.dropdown-menu > li, ul.dropdown-menu > li > ul > li { /* No decoration on bullet points in dropdown menu */ list-style-type: none; } .page { margin: 2em auto; width: 35em; border: 5px solid #ccc; padding: 0.8em; background: white; } .entries { list-style: none; margin: 0; padding: 0; } .entries li { margin: 0.8em 1.2em; } .entries li h2 { margin-left: -1em; } .add-entry { font-size: 0.9em; border-bottom: 1px solid #ccc; } .add-entry dl { font-weight: bold; } .metanav { text-align: right; font-size: 0.8em; padding: 0.3em; margin-bottom: 1em; background: #fafafa; } .flash { background: #cee5F5; padding: 0.5em; border: 1px solid #aacbe2; } .error { background: #f0d6d6; padding: 0.5em; } .file-found { color: #23BA49; } .file-notfound { color: #FF4747; } /* Bootstrap custom styling to correctly render multiple * form-controls in an input-group: * github.com/twbs/bootstrap/issues/12732 */ .input-group-field { display: table-cell; vertical-align: middle; border-radius:4px; min-width:1%; white-space: nowrap; } .input-group-field .form-control { border-radius: inherit !important; } .input-group-field:not(:first-child):not(:last-child) { border-radius:0; } .input-group-field:not(:first-child):not(:last-child) .form-control { border-left-width: 0; border-right-width: 0; } .input-group-field:last-child { border-top-left-radius:0; border-bottom-left-radius:0; } .input-group > span:not(:last-child) > button { border-radius: 0; } .multi-input-group > .input-group-btn { vertical-align: bottom; padding: 0; } .dataTables_filter { margin-top: 15px; } .dataTables_filter input { width: 70%; float: right; } tr.api-doc-route-upcoming > td, tr.api-doc-route-upcoming > td > a { font-size: 90%; } tr.api-doc-route-deprecated > td, tr.api-doc-route-deprecated > td > a { color: red; } #back-to-top { display: initial; position: fixed; bottom: 30px; right: 30px; z-index: 10; } #back-to-top a img { display: block; width: 32px; height: 32px; background-size: 32px 32px; text-indent: -999px; overflow: hidden; } .table > thead > tr > th { border-bottom: 1px solid #e20026; } .table > tbody > tr > td { border-style: none; } pre { background-color: hsl(47, 99%, 75%); } .dataTables_wrapper { position: static; } /* breadcrumbs */ .bread-crumbs{ display: inline-block; margin: 0 0 15px; overflow: hidden; color: rgba(0, 0, 0, 0.55); font-size: 1.2rem; } bread-crumbs ul { list-style-type: none; } .bread-crumbs li { float: left; margin-right: 10px; list-style-type: none; } .bread-crumbs a { color: rgba(0, 0, 0, 0.75); border-bottom-style: none; } .bread-crumbs a:hover { color: rgba(0, 0, 0, 0.85); text-decoration: underline; } .title-small .bread-crumbs{ margin: -30px 0 25px; } #footer { background-color: #262626; color: hsl(0, 0%, 100%); font-size: 1.2rem; text-align: center; padding-top: 20px; padding-bottom: 20px; position: absolute; bottom: 0; left: 0; right: 0; } #footer a, #footer a:visited { color: hsl(0, 0%, 100%); } #footer a:hover { text-decoration: underline; } .highlightjs pre { background-color: transparent; border-radius: 0px; border-color: transparent; } .swh-metadata-table-row pre { background-color: #f5f5f5; } .hljs { background-color: transparent; white-space: pre; } - + .scrollable-menu { max-height: 180px; overflow-x: hidden; } .swh-browse-top-navigation { border-bottom: 1px solid #ddd; min-height: 42px; padding: 4px 5px 0px 5px; } .swh-browse-bread-crumbs { font-size: inherit; vertical-align: text-top; margin-bottom: 1px; } .swh-metadata-table-row { border-top: 1px solid #ddd !important; } /* for block of numbers */ td.hljs-ln-numbers { -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; text-align: center; color: #ccc; border-right: 1px solid #CCC; vertical-align: top; padding-right: 5px; /* your custom style here */ } /* for block of code */ td.hljs-ln-code { padding-left: 10px; } -.btn-swh { - color: #6C6C6C; - background-color: #EAEAEA; - border-color: #EAEAEA; - } - - .btn-swh:hover, - .btn-swh:focus, - .btn-swh:active, - .btn-swh.active, - .open .dropdown-toggle.btn-swh { - color: #6C6C6C; - background-color: #D4D4D4; - border-color: #EAEAEA; - } - - .btn-swh:active, - .btn-swh.active, - .open .dropdown-toggle.btn-swh { - background-image: none; - } - - .btn-swh.disabled, - .btn-swh[disabled], - fieldset[disabled] .btn-swh, - .btn-swh.disabled:hover, - .btn-swh[disabled]:hover, - fieldset[disabled] .btn-swh:hover, - .btn-swh.disabled:focus, - .btn-swh[disabled]:focus, - fieldset[disabled] .btn-swh:focus, - .btn-swh.disabled:active, - .btn-swh[disabled]:active, - fieldset[disabled] .btn-swh:active, - .btn-swh.disabled.active, - .btn-swh[disabled].active, - fieldset[disabled] .btn-swh.active { - background-color: #EAEAEA; - border-color: #EAEAEA; - } - - .btn-swh .badge { - color: #EAEAEA; - background-color: #6C6C6C; - } \ No newline at end of file +.btn-swh { + color: #6C6C6C; + background-color: #EAEAEA; + border-color: #EAEAEA; +} + +.btn-swh:hover, +.btn-swh:focus, +.btn-swh:active, +.btn-swh.active, +.open .dropdown-toggle.btn-swh { + color: #6C6C6C; + background-color: #D4D4D4; + border-color: #EAEAEA; +} + +.btn-swh:active, +.btn-swh.active, +.open .dropdown-toggle.btn-swh { + background-image: none; +} + +.btn-swh.disabled, +.btn-swh[disabled], +fieldset[disabled] .btn-swh, +.btn-swh.disabled:hover, +.btn-swh[disabled]:hover, +fieldset[disabled] .btn-swh:hover, +.btn-swh.disabled:focus, +.btn-swh[disabled]:focus, +fieldset[disabled] .btn-swh:focus, +.btn-swh.disabled:active, +.btn-swh[disabled]:active, +fieldset[disabled] .btn-swh:active, +.btn-swh.disabled.active, +.btn-swh[disabled].active, +fieldset[disabled] .btn-swh.active { + background-color: #EAEAEA; + border-color: #EAEAEA; +} + +.btn-swh .badge { + color: #EAEAEA; + background-color: #6C6C6C; +} + +.swh-http-error { + margin: 0 auto; + text-align: center; +} + +.swh-http-error-head { + color: #2d353c; + font-size: 30px; +} + +.swh-http-error-code { + bottom: 60%; + color: #2d353c; + font-size: 96px; + line-height: 80px; + margin-bottom: 10px!important; +} + +.swh-http-error-desc { + font-size: 12px; + color: #647788; +} + +.swh-http-error-desc pre { + background-color: #f5f5f5; + text-align: left; +} diff --git a/swh/web/static/img/swh-logo.svg b/swh/web/static/img/swh-logo.svg new file mode 100644 index 00000000..44181c3c --- /dev/null +++ b/swh/web/static/img/swh-logo.svg @@ -0,0 +1,148 @@ + + + +image/svg+xml + + + + + \ No newline at end of file diff --git a/swh/web/templates/error.html b/swh/web/templates/error.html new file mode 100644 index 00000000..de5b34e2 --- /dev/null +++ b/swh/web/templates/error.html @@ -0,0 +1,31 @@ +{% extends "layout.html" %} +{% load static %} + +{% block content %} + +
+
Error
+
+ + {{ error_code }} + +
+

{{ error_message }}

+ +
+
{{ error_description }}
+ +
+
+ +{% endblock %} \ No newline at end of file diff --git a/swh/web/tests/browse/views/test_content.py b/swh/web/tests/browse/views/test_content.py index d5ccd3b1..4bce0b70 100644 --- a/swh/web/tests/browse/views/test_content.py +++ b/swh/web/tests/browse/views/test_content.py @@ -1,228 +1,246 @@ # Copyright (C) 2017 The Software Heritage developers # See the AUTHORS file at the top-level directory of this distribution # License: GNU General Public License version 3, or any later version # See top-level LICENSE file for more information import base64 from unittest.mock import patch from nose.tools import istest from django.test import TestCase from django.utils.html import escape from django.utils.encoding import DjangoUnicodeDecodeError +from swh.web.common.exc import NotFoundExc from swh.web.common.utils import reverse from swh.web.browse.utils import ( gen_path_info ) from .data.content_test_data import ( stub_content_text_data, stub_content_text_path_with_root_dir, stub_content_bin_data, stub_content_bin_filename, stub_content_text_no_highlight_data, non_utf8_encoded_content_data, non_utf8_encoded_content, non_utf8_encoding ) class SwhBrowseContentTest(TestCase): @patch('swh.web.browse.views.content.request_content') @istest def content_view_text(self, mock_request_content): mock_request_content.return_value = stub_content_text_data url = reverse('browse-content', kwargs={'query_string': stub_content_text_data['checksums']['sha1']}) # noqa url_raw = reverse('browse-content-raw', kwargs={'query_string': stub_content_text_data['checksums']['sha1']}) # noqa resp = self.client.get(url) self.assertEquals(resp.status_code, 200) self.assertTemplateUsed('content.html') self.assertContains(resp, '') self.assertContains(resp, escape(stub_content_text_data['raw_data'])) self.assertContains(resp, url_raw) @patch('swh.web.browse.views.content.request_content') @istest def content_view_text_no_highlight(self, mock_request_content): mock_request_content.return_value = stub_content_text_no_highlight_data url = reverse('browse-content', kwargs={'query_string': stub_content_text_no_highlight_data['checksums']['sha1']}) # noqa url_raw = reverse('browse-content-raw', kwargs={'query_string': stub_content_text_no_highlight_data['checksums']['sha1']}) # noqa resp = self.client.get(url) self.assertEquals(resp.status_code, 200) self.assertTemplateUsed('content.html') self.assertContains(resp, '') self.assertContains(resp, escape(stub_content_text_no_highlight_data['raw_data'])) # noqa self.assertContains(resp, url_raw) @patch('swh.web.browse.utils.service') @istest def content_view_no_utf8_text(self, mock_service): mock_service.lookup_content.return_value = \ non_utf8_encoded_content_data mock_service.lookup_content_raw.return_value = \ {'data': non_utf8_encoded_content} mock_service.lookup_content_filetype.return_value = None mock_service.lookup_content_language.return_value = None mock_service.lookup_content_license.return_value = None url = reverse('browse-content', kwargs={'query_string': non_utf8_encoded_content_data['checksums']['sha1']}) # noqa try: resp = self.client.get(url) self.assertEquals(resp.status_code, 200) self.assertTemplateUsed('content.html') self.assertContains(resp, escape(non_utf8_encoded_content.decode(non_utf8_encoding).encode('utf-8'))) # noqa except DjangoUnicodeDecodeError: self.fail('Textual content is not encoded in utf-8') @patch('swh.web.browse.views.content.request_content') @istest def content_view_image(self, mock_request_content): mime_type = 'image/png' mock_request_content.return_value = stub_content_bin_data url = reverse('browse-content', kwargs={'query_string': stub_content_bin_data['checksums']['sha1']}) # noqa url_raw = reverse('browse-content-raw', kwargs={'query_string': stub_content_bin_data['checksums']['sha1']}) # noqa resp = self.client.get(url) self.assertEquals(resp.status_code, 200) self.assertTemplateUsed('content.html') pngEncoded = base64.b64encode(stub_content_bin_data['raw_data']) \ .decode('utf-8') self.assertContains(resp, '' % (mime_type, pngEncoded)) self.assertContains(resp, url_raw) @patch('swh.web.browse.views.content.request_content') @istest def content_view_with_path(self, mock_request_content): mock_request_content.return_value = stub_content_text_data url = reverse('browse-content', kwargs={'query_string': stub_content_text_data['checksums']['sha1']}, # noqa query_params={'path': stub_content_text_path_with_root_dir}) # noqa resp = self.client.get(url) self.assertEquals(resp.status_code, 200) self.assertTemplateUsed('content.html') self.assertContains(resp, '') self.assertContains(resp, escape(stub_content_text_data['raw_data'])) split_path = stub_content_text_path_with_root_dir.split('/') root_dir_sha1 = split_path[0] filename = split_path[-1] path = stub_content_text_path_with_root_dir \ .replace(root_dir_sha1 + '/', '') \ .replace(filename, '') path_info = gen_path_info(path) root_dir_url = reverse('browse-directory', kwargs={'sha1_git': root_dir_sha1}) self.assertContains(resp, '
  • ', count=len(path_info)+1) self.assertContains(resp, '' + root_dir_sha1[:7] + '') for p in path_info: dir_url = reverse('browse-directory', kwargs={'sha1_git': root_dir_sha1, 'path': p['path']}) self.assertContains(resp, '' + p['name'] + '') self.assertContains(resp, '
  • ' + filename + '
  • ') url_raw = reverse('browse-content-raw', kwargs={'query_string': stub_content_text_data['checksums']['sha1']}, # noqa query_params={'filename': filename}) self.assertContains(resp, url_raw) @patch('swh.web.browse.views.content.request_content') @istest def test_content_raw_text(self, mock_request_content): mock_request_content.return_value = stub_content_text_data url = reverse('browse-content-raw', kwargs={'query_string': stub_content_text_data['checksums']['sha1']}) # noqa resp = self.client.get(url) self.assertEquals(resp.status_code, 200) self.assertEqual(resp['Content-Type'], 'text/plain') self.assertEqual(resp['Content-disposition'], 'filename=%s_%s' % ('sha1', stub_content_text_data['checksums']['sha1'])) # noqa self.assertEqual(resp.content, stub_content_text_data['raw_data']) filename = stub_content_text_path_with_root_dir.split('/')[-1] url = reverse('browse-content-raw', kwargs={'query_string': stub_content_text_data['checksums']['sha1']}, # noqa query_params={'filename': filename}) resp = self.client.get(url) self.assertEquals(resp.status_code, 200) self.assertEqual(resp['Content-Type'], 'text/plain') self.assertEqual(resp['Content-disposition'], 'filename=%s' % filename) self.assertEqual(resp.content, stub_content_text_data['raw_data']) @patch('swh.web.browse.views.content.request_content') @istest def content_raw_bin(self, mock_request_content): mock_request_content.return_value = stub_content_bin_data url = reverse('browse-content-raw', kwargs={'query_string': stub_content_bin_data['checksums']['sha1']}) # noqa resp = self.client.get(url) self.assertEquals(resp.status_code, 200) self.assertEqual(resp['Content-Type'], 'application/octet-stream') self.assertEqual(resp['Content-disposition'], 'attachment; filename=%s_%s' % ('sha1', stub_content_bin_data['checksums']['sha1'])) self.assertEqual(resp.content, stub_content_bin_data['raw_data']) url = reverse('browse-content-raw', kwargs={'query_string': stub_content_bin_data['checksums']['sha1']}, # noqa query_params={'filename': stub_content_bin_filename}) resp = self.client.get(url) self.assertEquals(resp.status_code, 200) self.assertEqual(resp['Content-Type'], 'application/octet-stream') self.assertEqual(resp['Content-disposition'], 'attachment; filename=%s' % stub_content_bin_filename) self.assertEqual(resp.content, stub_content_bin_data['raw_data']) + + @patch('swh.web.browse.views.content.request_content') + @istest + def content_request_errors(self, mock_request_content): + + url = reverse('browse-content', kwargs={'query_string': '123456'}) + resp = self.client.get(url) + self.assertEquals(resp.status_code, 400) + self.assertTemplateUsed('error.html') + + mock_request_content.side_effect = NotFoundExc('content not found') + + url = reverse('browse-content', + kwargs={'query_string': stub_content_text_data['checksums']['sha1']}) # noqa + resp = self.client.get(url) + self.assertEquals(resp.status_code, 404) + self.assertTemplateUsed('error.html') diff --git a/swh/web/tests/browse/views/test_directory.py b/swh/web/tests/browse/views/test_directory.py index bac55edd..68f45b1d 100644 --- a/swh/web/tests/browse/views/test_directory.py +++ b/swh/web/tests/browse/views/test_directory.py @@ -1,99 +1,126 @@ # Copyright (C) 2017 The Software Heritage developers # See the AUTHORS file at the top-level directory of this distribution # License: GNU General Public License version 3, or any later version # See top-level LICENSE file for more information from unittest.mock import patch from nose.tools import istest, nottest from django.test import TestCase +from swh.web.common.exc import BadInputExc, NotFoundExc from swh.web.common.utils import reverse from swh.web.browse.utils import gen_path_info from .data.directory_test_data import ( stub_root_directory_sha1, stub_root_directory_data, stub_sub_directory_path, stub_sub_directory_data ) class SwhBrowseDirectoryTest(TestCase): @nottest def directory_view(self, root_directory_sha1, directory_entries, path=None): dirs = [e for e in directory_entries if e['type'] == 'dir'] files = [e for e in directory_entries if e['type'] == 'file'] url_args = {'sha1_git': root_directory_sha1} if path: url_args['path'] = path url = reverse('browse-directory', kwargs=url_args) root_dir_url = reverse('browse-directory', kwargs={'sha1_git': root_directory_sha1}) resp = self.client.get(url) self.assertEquals(resp.status_code, 200) self.assertTemplateUsed('directory.html') self.assertContains(resp, '' + root_directory_sha1[:7] + '') self.assertContains(resp, '', count=len(dirs)) self.assertContains(resp, '', count=len(files)) for d in dirs: dir_path = d['name'] if path: dir_path = "%s/%s" % (path, d['name']) dir_url = reverse('browse-directory', kwargs={'sha1_git': root_directory_sha1, 'path': dir_path}) self.assertContains(resp, dir_url) for f in files: file_path = "%s/%s" % (root_directory_sha1, f['name']) if path: file_path = "%s/%s/%s" % (root_directory_sha1, path, f['name']) query_string = 'sha1_git:' + f['target'] file_url = reverse('browse-content', kwargs={'query_string': query_string}, query_params={'path': file_path}) self.assertContains(resp, file_url) path_info = gen_path_info(path) self.assertContains(resp, '
  • ', count=len(path_info)+1) self.assertContains(resp, '%s' % (root_dir_url, root_directory_sha1[:7])) for p in path_info: dir_url = reverse('browse-directory', kwargs={'sha1_git': root_directory_sha1, 'path': p['path']}) self.assertContains(resp, '%s' % (dir_url, p['name'])) @patch('swh.web.browse.utils.service') @istest def root_directory_view(self, mock_service): mock_service.lookup_directory.return_value = \ stub_root_directory_data self.directory_view(stub_root_directory_sha1, stub_root_directory_data) @patch('swh.web.browse.utils.service') @patch('swh.web.browse.views.directory.service') @istest def sub_directory_view(self, mock_directory_service, mock_utils_service): mock_utils_service.lookup_directory.return_value = \ stub_sub_directory_data mock_directory_service.lookup_directory_with_path.return_value = \ {'target': '120c39eeb566c66a77ce0e904d29dfde42228adc'} self.directory_view(stub_root_directory_sha1, stub_sub_directory_data, stub_sub_directory_path) + + @patch('swh.web.browse.utils.service') + @patch('swh.web.browse.views.directory.service') + @istest + def directory_request_errors(self, mock_directory_service, + mock_utils_service): + + mock_utils_service.lookup_directory.side_effect = \ + BadInputExc('directory not found') + + dir_url = reverse('browse-directory', + kwargs={'sha1_git': '1253456'}) + + resp = self.client.get(dir_url) + self.assertEquals(resp.status_code, 400) + self.assertTemplateUsed('error.html') + + mock_utils_service.lookup_directory.side_effect = \ + NotFoundExc('directory not found') + + dir_url = reverse('browse-directory', + kwargs={'sha1_git': '1253456'}) + + resp = self.client.get(dir_url) + self.assertEquals(resp.status_code, 404) + self.assertTemplateUsed('error.html') diff --git a/swh/web/tests/browse/views/test_origin.py b/swh/web/tests/browse/views/test_origin.py index d54d6fb5..3714a314 100644 --- a/swh/web/tests/browse/views/test_origin.py +++ b/swh/web/tests/browse/views/test_origin.py @@ -1,387 +1,499 @@ # Copyright (C) 2017 The Software Heritage developers # See the AUTHORS file at the top-level directory of this distribution # License: GNU General Public License version 3, or any later version # See top-level LICENSE file for more information from unittest.mock import patch from nose.tools import istest, nottest from django.test import TestCase from django.utils.html import escape +from swh.web.common.exc import NotFoundExc from swh.web.common.utils import reverse from .data.origin_test_data import ( origin_info_test_data, origin_visits_test_data, stub_content_origin_id, stub_content_origin_visit_id, stub_content_origin_visit_unix_ts, stub_content_origin_visit_iso_date, stub_content_origin_branch, stub_content_origin_visits, stub_content_origin_branches, stub_origin_id, stub_visit_id, stub_origin_visits, stub_origin_branches, stub_origin_root_directory_entries, stub_origin_master_branch, stub_origin_root_directory_sha1, stub_origin_sub_directory_path, stub_origin_sub_directory_entries, stub_visit_unix_ts, stub_visit_iso_date ) from .data.content_test_data import ( stub_content_root_dir, stub_content_text_data, stub_content_text_path ) from swh.web.browse.utils import ( gen_path_info ) class SwhBrowseOriginTest(TestCase): @patch('swh.web.browse.views.origin.get_origin_visits') @patch('swh.web.browse.views.origin.service') @istest def test_origin_browse(self, mock_service, mock_get_origin_visits): mock_service.lookup_origin.return_value = origin_info_test_data mock_get_origin_visits.return_value = origin_visits_test_data url = reverse('browse-origin', kwargs={'origin_id': origin_info_test_data['id']}) resp = self.client.get(url) self.assertEquals(resp.status_code, 200) self.assertTemplateUsed('origin.html') self.assertContains(resp, '%s' % origin_info_test_data['id']) self.assertContains(resp, '
    %s
    ' % origin_info_test_data['type']) # noqa self.assertContains(resp, '
    %s
    ' % (origin_info_test_data['url'], origin_info_test_data['url'])) self.assertContains(resp, '', count=len(origin_visits_test_data)) for visit in origin_visits_test_data: browse_url = reverse('browse-origin-directory', kwargs={'origin_id': visit['origin'], 'visit_id': visit['visit']}) self.assertContains(resp, '%s' % (browse_url, browse_url)) @nottest def origin_content_view_test(self, origin_id, origin_visits, origin_branches, origin_branch, root_dir_sha1, content_sha1, content_path, content_data, content_language, visit_id=None, ts=None): url_args = {'origin_id': origin_id, 'path': content_path} if not visit_id: visit_id = origin_visits[-1]['visit'] if ts: url_args['timestamp'] = ts else: url_args['visit_id'] = visit_id url = reverse('browse-origin-content', kwargs=url_args) resp = self.client.get(url) self.assertEquals(resp.status_code, 200) self.assertTemplateUsed('content.html') self.assertContains(resp, '' % content_language) self.assertContains(resp, escape(content_data)) split_path = content_path.split('/') filename = split_path[-1] path = content_path.replace(filename, '')[:-1] path_info = gen_path_info(path) del url_args['path'] root_dir_url = reverse('browse-origin-directory', kwargs=url_args, query_params={'branch': origin_branch}) self.assertContains(resp, '
  • ', count=len(path_info)+1) self.assertContains(resp, '%s' % (root_dir_url, root_dir_sha1[:7])) for p in path_info: url_args['path'] = p['path'] dir_url = reverse('browse-origin-directory', kwargs=url_args, query_params={'branch': origin_branch}) self.assertContains(resp, '%s' % (dir_url, p['name'])) self.assertContains(resp, '
  • %s
  • ' % filename) query_string = 'sha1_git:' + content_sha1 url_raw = reverse('browse-content-raw', kwargs={'query_string': query_string}, query_params={'filename': filename}) self.assertContains(resp, url_raw) self.assertContains(resp, '
  • ', count=len(origin_branches)) url_args['path'] = content_path for branch in origin_branches: root_dir_branch_url = \ reverse('browse-origin-content', kwargs=url_args, query_params={'branch': branch['name']}) self.assertContains(resp, '%s' % (root_dir_branch_url, branch['name'])) @patch('swh.web.browse.views.origin.get_origin_visits') @patch('swh.web.browse.views.origin.get_origin_visit_branches') @patch('swh.web.browse.views.origin.service') @patch('swh.web.browse.views.origin.request_content') @istest def origin_content_view(self, mock_request_content, mock_service, mock_get_origin_visit_branches, mock_get_origin_visits): stub_content_text_sha1 = stub_content_text_data['checksums']['sha1'] mock_get_origin_visits.return_value = stub_content_origin_visits mock_get_origin_visit_branches.return_value = stub_content_origin_branches # noqa mock_service.lookup_directory_with_path.return_value = \ {'target': stub_content_text_sha1} mock_request_content.return_value = stub_content_text_data self.origin_content_view_test(stub_content_origin_id, stub_content_origin_visits, stub_content_origin_branches, stub_content_origin_branch, stub_content_root_dir, stub_content_text_sha1, stub_content_text_path, stub_content_text_data['raw_data'], 'cpp') self.origin_content_view_test(stub_content_origin_id, stub_content_origin_visits, stub_content_origin_branches, stub_content_origin_branch, stub_content_root_dir, stub_content_text_sha1, stub_content_text_path, stub_content_text_data['raw_data'], 'cpp', visit_id=stub_content_origin_visit_id) self.origin_content_view_test(stub_content_origin_id, stub_content_origin_visits, stub_content_origin_branches, stub_content_origin_branch, stub_content_root_dir, stub_content_text_sha1, stub_content_text_path, stub_content_text_data['raw_data'], 'cpp', ts=stub_content_origin_visit_unix_ts) self.origin_content_view_test(stub_content_origin_id, stub_content_origin_visits, stub_content_origin_branches, stub_content_origin_branch, stub_content_root_dir, stub_content_text_sha1, stub_content_text_path, stub_content_text_data['raw_data'], 'cpp', ts=stub_content_origin_visit_iso_date) @nottest def origin_directory_view(self, origin_id, origin_visits, origin_branches, origin_branch, root_directory_sha1, directory_entries, visit_id=None, ts=None, path=None): dirs = [e for e in directory_entries if e['type'] == 'dir'] files = [e for e in directory_entries if e['type'] == 'file'] if not visit_id: visit_id = origin_visits[-1]['visit'] url_args = {'origin_id': origin_id} if ts: url_args['timestamp'] = ts else: url_args['visit_id'] = visit_id if path: url_args['path'] = path url = reverse('browse-origin-directory', kwargs=url_args) resp = self.client.get(url) self.assertEquals(resp.status_code, 200) self.assertTemplateUsed('directory.html') self.assertContains(resp, '', count=len(dirs)) self.assertContains(resp, '', count=len(files)) for d in dirs: dir_path = d['name'] if path: dir_path = "%s/%s" % (path, d['name']) dir_url_args = dict(url_args) dir_url_args['path'] = dir_path dir_url = reverse('browse-origin-directory', kwargs=dir_url_args, query_params={'branch': origin_branch}) # noqa self.assertContains(resp, dir_url) for f in files: file_path = f['name'] if path: file_path = "%s/%s" % (path, f['name']) file_url_args = dict(url_args) file_url_args['path'] = file_path file_url = reverse('browse-origin-content', kwargs=file_url_args, query_params={'branch': origin_branch}) # noqa self.assertContains(resp, file_url) if 'path' in url_args: del url_args['path'] root_dir_branch_url = \ reverse('browse-origin-directory', kwargs=url_args, query_params={'branch': origin_branch}) nb_bc_paths = 1 if path: nb_bc_paths = len(path.split('/')) + 1 self.assertContains(resp, '
  • ', count=nb_bc_paths) self.assertContains(resp, '%s' % (root_dir_branch_url, root_directory_sha1[:7])) self.assertContains(resp, '
  • ', count=len(origin_branches)) if path: url_args['path'] = path for branch in origin_branches: root_dir_branch_url = \ reverse('browse-origin-directory', kwargs=url_args, query_params={'branch': branch['name']}) self.assertContains(resp, '%s' % (root_dir_branch_url, branch['name'])) @patch('swh.web.browse.views.origin.get_origin_visits') @patch('swh.web.browse.views.origin.get_origin_visit_branches') @patch('swh.web.browse.utils.service') @patch('swh.web.browse.views.origin.service') @istest def origin_root_directory_view(self, mock_origin_service, mock_utils_service, mock_get_origin_visit_branches, mock_get_origin_visits): mock_get_origin_visits.return_value = stub_origin_visits mock_get_origin_visit_branches.return_value = stub_origin_branches mock_utils_service.lookup_directory.return_value = \ stub_origin_root_directory_entries mock_origin_service.lookup_origin.return_value = origin_info_test_data self.origin_directory_view(stub_origin_id, stub_origin_visits, stub_origin_branches, stub_origin_master_branch, stub_origin_root_directory_sha1, stub_origin_root_directory_entries) self.origin_directory_view(stub_origin_id, stub_origin_visits, stub_origin_branches, stub_origin_master_branch, stub_origin_root_directory_sha1, stub_origin_root_directory_entries, visit_id=stub_visit_id) self.origin_directory_view(stub_origin_id, stub_origin_visits, stub_origin_branches, stub_origin_master_branch, stub_origin_root_directory_sha1, stub_origin_root_directory_entries, ts=stub_visit_unix_ts) self.origin_directory_view(stub_origin_id, stub_origin_visits, stub_origin_branches, stub_origin_master_branch, stub_origin_root_directory_sha1, stub_origin_root_directory_entries, ts=stub_visit_iso_date) @patch('swh.web.browse.views.origin.get_origin_visits') @patch('swh.web.browse.views.origin.get_origin_visit_branches') @patch('swh.web.browse.utils.service') @patch('swh.web.browse.views.origin.service') @istest def origin_sub_directory_view(self, mock_origin_service, mock_utils_service, mock_get_origin_visit_branches, mock_get_origin_visits): mock_get_origin_visits.return_value = stub_origin_visits mock_get_origin_visit_branches.return_value = stub_origin_branches mock_utils_service.lookup_directory.return_value = \ stub_origin_sub_directory_entries mock_origin_service.lookup_directory_with_path.return_value = \ {'target': '120c39eeb566c66a77ce0e904d29dfde42228adb'} self.origin_directory_view(stub_origin_id, stub_origin_visits, stub_origin_branches, stub_origin_master_branch, stub_origin_root_directory_sha1, stub_origin_sub_directory_entries, path=stub_origin_sub_directory_path) self.origin_directory_view(stub_origin_id, stub_origin_visits, stub_origin_branches, stub_origin_master_branch, stub_origin_root_directory_sha1, stub_origin_sub_directory_entries, visit_id=stub_visit_id, path=stub_origin_sub_directory_path) self.origin_directory_view(stub_origin_id, stub_origin_visits, stub_origin_branches, stub_origin_master_branch, stub_origin_root_directory_sha1, stub_origin_sub_directory_entries, ts=stub_visit_unix_ts, path=stub_origin_sub_directory_path) self.origin_directory_view(stub_origin_id, stub_origin_visits, stub_origin_branches, stub_origin_master_branch, stub_origin_root_directory_sha1, stub_origin_sub_directory_entries, ts=stub_visit_iso_date, path=stub_origin_sub_directory_path) + + @patch('swh.web.browse.views.origin.request_content') + @patch('swh.web.browse.views.origin.get_origin_visits') + @patch('swh.web.browse.views.origin.get_origin_visit_branches') + @patch('swh.web.browse.utils.service') + @patch('swh.web.browse.views.origin.service') + @istest + def origin_request_errors(self, mock_origin_service, + mock_utils_service, + mock_get_origin_visit_branches, + mock_get_origin_visits, + mock_request_content): + + mock_origin_service.lookup_origin.side_effect = \ + NotFoundExc('origin not found') + url = reverse('browse-origin', + kwargs={'origin_id': '1'}) + resp = self.client.get(url) + self.assertEquals(resp.status_code, 404) + self.assertTemplateUsed('error.html') + self.assertContains(resp, "origin not found", status_code=404) + + mock_origin_service.lookup_origin.side_effect = None + mock_origin_service.lookup_origin.return_value = origin_info_test_data + mock_get_origin_visits.return_value = [] + url = reverse('browse-origin-directory', + kwargs={'origin_id': '1'}) + resp = self.client.get(url) + self.assertEquals(resp.status_code, 404) + self.assertTemplateUsed('error.html') + self.assertContains(resp, "No SWH visit", status_code=404) + + mock_get_origin_visits.return_value = stub_origin_visits + mock_get_origin_visit_branches.side_effect = \ + NotFoundExc('visit not found') + url = reverse('browse-origin-directory', + kwargs={'origin_id': '1', + 'visit_id': len(stub_origin_visits)+1}) + resp = self.client.get(url) + self.assertEquals(resp.status_code, 404) + self.assertTemplateUsed('error.html') + self.assertContains(resp, 'visit not found', status_code=404) + + mock_get_origin_visits.return_value = stub_origin_visits + mock_get_origin_visit_branches.side_effect = None + mock_get_origin_visit_branches.return_value = [] + url = reverse('browse-origin-directory', + kwargs={'origin_id': '1'}) + resp = self.client.get(url) + self.assertEquals(resp.status_code, 404) + self.assertTemplateUsed('error.html') + self.assertRegex(resp.content.decode('utf-8'), + 'Branch HEAD.*not found') + + mock_get_origin_visit_branches.return_value = stub_origin_branches + mock_utils_service.lookup_directory.side_effect = \ + NotFoundExc('Directory not found') + url = reverse('browse-origin-directory', + kwargs={'origin_id': '1'}) + resp = self.client.get(url) + self.assertEquals(resp.status_code, 404) + self.assertTemplateUsed('error.html') + self.assertContains(resp, 'Directory not found', status_code=404) + + mock_origin_service.lookup_origin.side_effect = None + mock_origin_service.lookup_origin.return_value = origin_info_test_data + mock_get_origin_visits.return_value = [] + url = reverse('browse-origin-content', + kwargs={'origin_id': '1', + 'path': 'foo'}) + resp = self.client.get(url) + self.assertEquals(resp.status_code, 404) + self.assertTemplateUsed('error.html') + self.assertContains(resp, "No SWH visit", status_code=404) + + mock_get_origin_visits.return_value = stub_origin_visits + mock_get_origin_visit_branches.side_effect = \ + NotFoundExc('visit not found') + url = reverse('browse-origin-content', + kwargs={'origin_id': '1', + 'visit_id': len(stub_origin_visits)+1, + 'path': 'foo'}) + resp = self.client.get(url) + self.assertEquals(resp.status_code, 404) + self.assertTemplateUsed('error.html') + self.assertContains(resp, 'visit not found', status_code=404) + + mock_get_origin_visits.return_value = stub_origin_visits + mock_get_origin_visit_branches.side_effect = None + mock_get_origin_visit_branches.return_value = [] + url = reverse('browse-origin-content', + kwargs={'origin_id': '1', + 'path': 'foo'}) + resp = self.client.get(url) + self.assertEquals(resp.status_code, 404) + self.assertTemplateUsed('error.html') + self.assertRegex(resp.content.decode('utf-8'), + 'Branch HEAD.*not found') + + mock_get_origin_visit_branches.return_value = stub_origin_branches + mock_origin_service.lookup_directory_with_path.return_value = \ + {'target': stub_content_text_data['checksums']['sha1']} + mock_request_content.side_effect = \ + NotFoundExc('Content not found') + url = reverse('browse-origin-content', + kwargs={'origin_id': '1', + 'path': 'foo'}) + resp = self.client.get(url) + self.assertEquals(resp.status_code, 404) + self.assertTemplateUsed('error.html') + self.assertContains(resp, 'Content not found', status_code=404) diff --git a/swh/web/tests/browse/views/test_person.py b/swh/web/tests/browse/views/test_person.py index 95c69323..416c0365 100644 --- a/swh/web/tests/browse/views/test_person.py +++ b/swh/web/tests/browse/views/test_person.py @@ -1,42 +1,55 @@ # Copyright (C) 2017 The Software Heritage developers # See the AUTHORS file at the top-level directory of this distribution # License: GNU General Public License version 3, or any later version # See top-level LICENSE file for more information from unittest.mock import patch from nose.tools import istest from django.test import TestCase +from swh.web.common.exc import NotFoundExc from swh.web.common.utils import reverse class SwhBrowsePersonTest(TestCase): @patch('swh.web.browse.views.person.service') @istest def person_browse(self, mock_service): test_person_data = \ { "email": "j.adams440@gmail.com", "fullname": "oysterCrusher ", "id": 457587, "name": "oysterCrusher" } mock_service.lookup_person.return_value = test_person_data url = reverse('browse-person', kwargs={'person_id': 457587}) resp = self.client.get(url) self.assertEquals(resp.status_code, 200) self.assertTemplateUsed('person.html') self.assertContains(resp, '
    %s
    ' % test_person_data['id']) self.assertContains(resp, '
    %s
    ' % test_person_data['name']) self.assertContains(resp, '
    %s
    ' % (test_person_data['email'], test_person_data['email'])) self.assertContains(resp, '
    %s <%s>
    ' % # noqa (test_person_data['name'], test_person_data['email'], test_person_data['email'])) + + @patch('swh.web.browse.views.person.service') + @istest + def person_request_error(self, mock_service): + mock_service.lookup_person.side_effect = \ + NotFoundExc('Person not found') + + url = reverse('browse-person', kwargs={'person_id': 457587}) + resp = self.client.get(url) + self.assertEquals(resp.status_code, 404) + self.assertTemplateUsed('error.html') + self.assertContains(resp, 'Person not found', status_code=404) diff --git a/swh/web/tests/browse/views/test_revision.py b/swh/web/tests/browse/views/test_revision.py index 925604a4..cf8debed 100644 --- a/swh/web/tests/browse/views/test_revision.py +++ b/swh/web/tests/browse/views/test_revision.py @@ -1,177 +1,199 @@ # Copyright (C) 2017 The Software Heritage developers # See the AUTHORS file at the top-level directory of this distribution # License: GNU General Public License version 3, or any later version # See top-level LICENSE file for more information from unittest.mock import patch from nose.tools import istest from django.test import TestCase from django.utils.html import escape +from swh.web.common.exc import NotFoundExc from swh.web.common.utils import reverse, format_utc_iso_date from .data.revision_test_data import ( revision_id_test, revision_metadata_test, revision_history_log_test ) class SwhBrowseRevisionTest(TestCase): @patch('swh.web.browse.views.revision.service') @istest - def test_revision_browse(self, mock_service): + def revision_browse(self, mock_service): mock_service.lookup_revision.return_value = revision_metadata_test url = reverse('browse-revision', kwargs={'sha1_git': revision_id_test}) author_id = revision_metadata_test['author']['id'] author_name = revision_metadata_test['author']['name'] committer_id = revision_metadata_test['committer']['id'] committer_name = revision_metadata_test['committer']['name'] dir_id = revision_metadata_test['directory'] author_url = reverse('browse-person', kwargs={'person_id': author_id}) committer_url = reverse('browse-person', kwargs={'person_id': committer_id}) directory_url = reverse('browse-directory', kwargs={'sha1_git': dir_id}) history_url = reverse('browse-revision-log', kwargs={'sha1_git': revision_id_test}) resp = self.client.get(url) self.assertEquals(resp.status_code, 200) self.assertTemplateUsed('revision.html') self.assertContains(resp, '%s' % (author_url, author_name)) self.assertContains(resp, '%s' % (committer_url, committer_name)) self.assertContains(resp, '%s' % (directory_url, dir_id)) self.assertContains(resp, '%s' % (history_url, history_url)) for parent in revision_metadata_test['parents']: parent_url = reverse('browse-revision', kwargs={'sha1_git': parent}) self.assertContains(resp, '%s' % (parent_url, parent)) author_date = revision_metadata_test['date'] committer_date = revision_metadata_test['committer_date'] message = revision_metadata_test['message'] self.assertContains(resp, format_utc_iso_date(author_date)) self.assertContains(resp, format_utc_iso_date(committer_date)) self.assertContains(resp, message) @patch('swh.web.browse.views.revision.service') @istest - def test_revision_log_browse(self, mock_service): + def revision_log_browse(self, mock_service): per_page = 10 mock_service.lookup_revision_log.return_value = \ revision_history_log_test[:per_page+1] url = reverse('browse-revision-log', kwargs={'sha1_git': revision_id_test}, query_params={'per_page': per_page}) resp = self.client.get(url) prev_rev = revision_history_log_test[per_page]['id'] next_page_url = reverse('browse-revision-log', kwargs={'sha1_git': prev_rev}, query_params={'revs_breadcrumb': revision_id_test, # noqa 'per_page': per_page}) self.assertEquals(resp.status_code, 200) self.assertTemplateUsed('revision-log.html') self.assertContains(resp, '', count=per_page) self.assertContains(resp, '
  • Newer
  • ') self.assertContains(resp, '
  • Older
  • ' % escape(next_page_url)) for log in revision_history_log_test[:per_page]: author_url = reverse('browse-person', kwargs={'person_id': log['author']['id']}) revision_url = reverse('browse-revision', kwargs={'sha1_git': log['id']}) directory_url = reverse('browse-directory', kwargs={'sha1_git': log['directory']}) self.assertContains(resp, '%s' % (author_url, log['author']['name'])) self.assertContains(resp, '%s' % (revision_url, log['id'][:7])) self.assertContains(resp, '%s' % (directory_url, 'Tree')) mock_service.lookup_revision_log.return_value = \ revision_history_log_test[per_page:2*per_page+1] resp = self.client.get(next_page_url) prev_prev_rev = revision_history_log_test[2*per_page]['id'] prev_page_url = reverse('browse-revision-log', kwargs={'sha1_git': revision_id_test}, query_params={'per_page': per_page}) next_page_url = reverse('browse-revision-log', kwargs={'sha1_git': prev_prev_rev}, query_params={'revs_breadcrumb': revision_id_test + '/' + prev_rev, # noqa 'per_page': per_page}) self.assertEquals(resp.status_code, 200) self.assertTemplateUsed('revision-log.html') self.assertContains(resp, '', count=per_page) self.assertContains(resp, '
  • Newer
  • ' % escape(prev_page_url)) self.assertContains(resp, '
  • Older
  • ' % escape(next_page_url)) mock_service.lookup_revision_log.return_value = \ revision_history_log_test[2*per_page:3*per_page+1] resp = self.client.get(next_page_url) prev_prev_prev_rev = revision_history_log_test[3*per_page]['id'] prev_page_url = reverse('browse-revision-log', kwargs={'sha1_git': prev_rev}, query_params={'revs_breadcrumb': revision_id_test, # noqa 'per_page': per_page}) next_page_url = reverse('browse-revision-log', kwargs={'sha1_git': prev_prev_prev_rev}, query_params={'revs_breadcrumb': revision_id_test + '/' + prev_rev + '/' + prev_prev_rev, # noqa 'per_page': per_page}) self.assertEquals(resp.status_code, 200) self.assertTemplateUsed('revision-log.html') self.assertContains(resp, '', count=per_page) self.assertContains(resp, '
  • Newer
  • ' % escape(prev_page_url)) self.assertContains(resp, '
  • Older
  • ' % escape(next_page_url)) mock_service.lookup_revision_log.return_value = \ revision_history_log_test[3*per_page:3*per_page+per_page//2] resp = self.client.get(next_page_url) prev_page_url = reverse('browse-revision-log', kwargs={'sha1_git': prev_prev_rev}, query_params={'revs_breadcrumb': revision_id_test + '/' + prev_rev, # noqa 'per_page': per_page}) self.assertEquals(resp.status_code, 200) self.assertTemplateUsed('revision-log.html') self.assertContains(resp, '', count=per_page//2) self.assertContains(resp, '
  • Older
  • ') self.assertContains(resp, '
  • Newer
  • ' % escape(prev_page_url)) + + @patch('swh.web.browse.views.revision.service') + @istest + def revision_request_errors(self, mock_service): + mock_service.lookup_revision.side_effect = \ + NotFoundExc('Revision not found') + url = reverse('browse-revision', + kwargs={'sha1_git': revision_id_test}) + resp = self.client.get(url) + self.assertEquals(resp.status_code, 404) + self.assertTemplateUsed('error.html') + self.assertContains(resp, 'Revision not found', status_code=404) + + mock_service.lookup_revision_log.side_effect = \ + NotFoundExc('Revision not found') + url = reverse('browse-revision-log', + kwargs={'sha1_git': revision_id_test}) + resp = self.client.get(url) + self.assertEquals(resp.status_code, 404) + self.assertTemplateUsed('error.html') + self.assertContains(resp, 'Revision not found', status_code=404) diff --git a/swh/web/urls.py b/swh/web/urls.py index b98fd2f5..5c6a766e 100644 --- a/swh/web/urls.py +++ b/swh/web/urls.py @@ -1,38 +1,48 @@ # Copyright (C) 2017 The Software Heritage developers # See the AUTHORS file at the top-level directory of this distribution # License: GNU General Public License version 3, or any later version # See top-level LICENSE file for more information import django -from django.conf.urls import url, include +from django.conf.urls import ( + url, include, handler400, handler403, handler404, handler500 +) from django.contrib.staticfiles.urls import staticfiles_urlpatterns from django.shortcuts import redirect from django.views.generic.base import RedirectView +from swh.web.common.exc import ( + swh_handle400, swh_handle403, swh_handle404, swh_handle500 +) + favicon_view = RedirectView.as_view(url='/static/img/icons/swh-logo-32x32.png', permanent=True) def default_view(request): return redirect('api_homepage') urlpatterns = [ url(r'^favicon\.ico$', favicon_view), url(r'^api/', include('swh.web.api.urls')), url(r'^browse/', include('swh.web.browse.urls')), - url(r'^$', default_view), + url(r'^$', default_view, name='swh-web-homepage'), ] - urlpatterns += staticfiles_urlpatterns() +handler400 = swh_handle400 # noqa +handler403 = swh_handle403 # noqa +handler404 = swh_handle404 # noqa +handler500 = swh_handle500 # noqa + # hack in order for our custom template tag library # to load on django 1.7 (debian jessie version) if django.VERSION < (1, 8): from django.template.base import templatetags_modules # noqa templatetags_modules += ['django.templatetags', 'django.contrib.admin.templatetags', 'django.contrib.staticfiles.templatetags', 'rest_framework.templatetags', 'swh.web.common']